Skip to content

[M1] Add menu bar UI shell#35

Merged
Peerapat-J merged 3 commits into
devfrom
PJ/m1-ui-shell
Jun 5, 2026
Merged

[M1] Add menu bar UI shell#35
Peerapat-J merged 3 commits into
devfrom
PJ/m1-ui-shell

Conversation

@Peerapat-J
Copy link
Copy Markdown
Owner

@Peerapat-J Peerapat-J commented Jun 5, 2026

Summary

  • Replace the starter window-first scaffold with a menu bar oriented app shell and shared app state.
  • Add Settings, Quick Translate, Translation Popup, Setup Guide, and Status surfaces backed by M1-safe mock state/services.
  • Add core popup, quick-translate, onboarding readiness, and settings models with focused tests.

Issues

Closes #7
Closes #10
Closes #11
Closes #12
Closes #13

Validation

  • git diff --check
  • swiftformat --lint . --config .swiftformat --cache ignore
  • swiftlint lint --strict --no-cache
  • swiftlint analyze --strict --compiler-log-path /private/tmp/linguistmac-m1-swiftlint-analyze-final3.log
  • xcodebuild -project LinguistMac.xcodeproj -scheme LinguistMac -configuration Debug -destination "platform=macOS" -derivedDataPath /private/tmp/linguistmac-m1-test-final3 CODE_SIGNING_ALLOWED=NO ENABLE_TESTABILITY=YES test
  • xcodebuild -project LinguistMac.xcodeproj -scheme LinguistMac -configuration Release -destination "platform=macOS" -derivedDataPath /private/tmp/linguistmac-m1-release CODE_SIGNING_ALLOWED=NO build
  • xcodebuild -project LinguistMac.xcodeproj -scheme LinguistMac -configuration Debug -destination "platform=macOS" -derivedDataPath /private/tmp/linguistmac-m1-analyze CODE_SIGNING_ALLOWED=NO SWIFT_TREAT_WARNINGS_AS_ERRORS=YES GCC_TREAT_WARNINGS_AS_ERRORS=YES analyze
  • CODE_SIGNING_ALLOWED=NO DERIVED_DATA_PATH=/private/tmp/linguistmac-m1-package ./script/build_and_run.sh --package

Summary by CodeRabbit

  • New Features
    • Menu bar integration for quick access to translation features
    • Onboarding workflow with permission setup checklist
    • Quick translate feature with language swapping capability
    • Customizable translation popup with font size and width adjustments
    • Comprehensive settings panel with language and provider selection
    • Recent translations history display in menu

@Peerapat-J Peerapat-J added this to the M1 Menu Bar + UI Shell milestone Jun 5, 2026
@Peerapat-J Peerapat-J added enhancement New feature or request type: feature User-visible product functionality. priority: p0 Critical path for initial parity. area: app App target, app identity, lifecycle, and platform integration. area: ui SwiftUI/AppKit UI surfaces. area: translation Translation coordination, language choices, and result state. area: settings Preferences, onboarding, and user configuration. area: shortcuts Global hotkeys and keyboard-driven workflows. labels Jun 5, 2026
@Peerapat-J Peerapat-J self-assigned this Jun 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 501fa90f-bfd7-4b7f-990e-700b0330ce5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch PJ/m1-ui-shell

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Peerapat-J Peerapat-J marked this pull request as ready for review June 5, 2026 14:47
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
Sources/LinguistMac/AppShellModel.swift (1)

231-244: ⚡ Quick win

Simplify clipboard service with @MainActor instead of actor.

Since both readText() and writeText() immediately dispatch to MainActor.run, wrapping the service in an actor adds isolation overhead without concurrency benefit. NSPasteboard requires main-thread access, so the entire type can be @MainActor.

♻️ Proposed refactor to use `@MainActor` directly
-actor SystemClipboardService: ClipboardServicing {
+@MainActor
+final class SystemClipboardService: ClipboardServicing {
     func readText() async -> String? {
-        await MainActor.run {
-            NSPasteboard.general.string(forType: .string)
-        }
+        NSPasteboard.general.string(forType: .string)
     }

     func writeText(_ text: String) async {
-        await MainActor.run {
-            NSPasteboard.general.clearContents()
-            NSPasteboard.general.setString(text, forType: .string)
-        }
+        NSPasteboard.general.clearContents()
+        NSPasteboard.general.setString(text, forType: .string)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LinguistMac/AppShellModel.swift` around lines 231 - 244, Replace the
actor-based SystemClipboardService with a main-isolated type by removing `actor`
and marking the type `@MainActor` so you avoid unnecessary actor isolation and
still satisfy NSPasteboard's main-thread requirement; update the declaration of
SystemClipboardService (which conforms to ClipboardServicing) and keep the
implementations of readText() and writeText() but remove the surrounding await
MainActor.run { ... } calls so they run on the main actor naturally and continue
to call NSPasteboard.general.string(forType: .string), clearContents(), and
setString(_:forType:) directly.
Sources/LinguistMac/TranslationPopupView.swift (1)

125-146: ⚡ Quick win

Prefer display names over raw values in user-facing error messages.

The error text interpolates kind.rawValue for permission kinds and providerID.rawValue for provider IDs, which may expose technical identifiers (e.g., "screenRecording", "deepl") rather than user-friendly names ("Screen Recording", "DeepL"). This reduces clarity for non-technical users.

♻️ Proposed improvement using display-friendly strings
 extension TranslationFailure {
     var displayText: String {
         switch self {
         case let .permissionDenied(kind):
-            "Permission required: \(kind.rawValue)."
+            "Permission required: \(kind.displayName)."
         case .captureCancelled:
             "Screen capture was cancelled."
         case .noTextRecognized:
             "No text was recognized."
         case .emptyInput:
             "Enter text before translating."
         case .unsupportedLanguagePair:
             "This language pair is not available yet."
         case let .providerUnavailable(providerID):
-            "Provider is unavailable: \(providerID.rawValue)."
+            "Translation provider \(providerID.displayName) is unavailable."
         case let .missingAPIKey(providerID):
-            "API key required for \(providerID.rawValue)."
+            "API key required for \(providerID.displayName)."
         case let .providerFailed(message):
             message
         }
     }
 }

If PermissionKind and TranslationProviderID don't have a displayName property, add one or maintain a lookup mapping in the extension.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LinguistMac/TranslationPopupView.swift` around lines 125 - 146, The
user-facing strings in TranslationFailure.displayText use kind.rawValue and
providerID.rawValue; update the .permissionDenied(kind),
.providerUnavailable(providerID) and .missingAPIKey(providerID) branches to use
a human-friendly display name instead (e.g., kind.displayName or a lookup for
PermissionKind and providerID.displayName or a TranslationProviderID-to-name
map) so messages show "Screen Recording" / "DeepL" style labels rather than
technical identifiers; add a displayName computed property or local mapping on
PermissionKind and TranslationProviderID if they don't exist and interpolate
that into the existing cases.
Sources/LinguistMac/OnboardingView.swift (1)

78-82: ⚡ Quick win

Extract duplicated Settings window opener to shared helper.

The openSettingsWindow() pattern using Selector(("showSettingsWindow:")) and NSApp.activate is duplicated in MenuBarMenuView.swift lines 109–113. Centralizing this logic reduces maintenance burden and ensures consistent behavior.

♻️ Proposed extraction to AppShellModel extension

Add to AppShellModel.swift:

extension AppShellModel {
    func openSettingsWindow() {
        record(.settings)
        NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
        NSApp.activate(ignoringOtherApps: true)
    }
}

Then update both call sites:

MenuBarMenuView.swift:

     private func openSettingsWindow() {
-        model.record(.settings)
-        NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
-        activateApp()
+        model.openSettingsWindow()
     }

OnboardingView.swift:

     private func openSettingsWindow() {
-        model.record(.settings)
-        NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
-        NSApp.activate(ignoringOtherApps: true)
+        model.openSettingsWindow()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LinguistMac/OnboardingView.swift` around lines 78 - 82, Extract the
duplicated settings-opening logic into an AppShellModel extension method and
update both call sites to use it: add an extension on AppShellModel with a new
func openSettingsWindow() that calls record(.settings),
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) and
NSApp.activate(ignoringOtherApps: true); then remove or replace the private
openSettingsWindow() in OnboardingView and the duplicate code in MenuBarMenuView
to call model.openSettingsWindow() instead so both use the shared
implementation.
Sources/LinguistMac/SettingsView.swift (1)

50-54: 💤 Low value

Consider hiding unimplemented launch-at-login control.

The toggle for "Launch at login" is permanently disabled with a caption explaining the feature will land later. Disabled controls reduce discoverability and can confuse users who attempt to interact with them.

♻️ Alternative: hide the toggle until the feature ships
                 Toggle("Auto-copy result", isOn: $model.settings.autoCopyEnabled)
-                Toggle("Launch at login", isOn: $model.settings.launchAtLoginEnabled)
-                    .disabled(true)
-                Text("Launch-at-login wiring lands with the app preferences milestone.")
-                    .font(.caption)
-                    .foregroundStyle(.secondary)
+                // Launch at login lands with M2 app preferences milestone

Or keep it visible but use a custom label with "(Coming soon)" suffix instead of a disabled toggle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LinguistMac/SettingsView.swift` around lines 50 - 54, The "Launch at
login" Toggle is permanently disabled and should be hidden or clarified; update
the view around Toggle("Launch at login", isOn:
$model.settings.launchAtLoginEnabled) so it is only shown when the feature flag
or milestone is enabled (e.g., wrap it in an if
model.settings.isLaunchAtLoginAvailable) or replace the disabled Toggle with a
non-interactive label that reads "Launch at login (Coming soon)" alongside the
explanatory Text; ensure you remove the .disabled(true) call if using the
"(Coming soon)" label and keep the explanatory caption Text("Launch-at-login
wiring lands with the app preferences milestone.") as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/LinguistMac/MenuBarMenuView.swift`:
- Around line 100-107: In summary(for result: TranslationResult) the truncation
check is off: change the condition so text lengths up to 27 are returned
unchanged and anything longer is truncated to 27 characters plus "..." (i.e.,
use text.count <= 27 instead of 30) so the else branch String(text.prefix(27)) +
"..." yields a shorter displayed string for longer inputs; locate the
summary(for:) function and update the threshold logic around the text variable
accordingly.

In `@Sources/LinguistMac/SettingsView.swift`:
- Around line 89-100: The .appleTranslation and .cloudProvider branches in the
ReadinessRow action currently only call model.record(.settings) and do not open
any UI; change those branches to call model.openSettingsWindow() (matching
OnboardingView.swift) and then record the event (i.e., call
model.openSettingsWindow() before model.record(.settings)) so clicking "Open"
opens the settings window and still logs the action; locate these changes in the
ForEach over model.readiness.items and the ReadinessRow closure where item.kind
is switched.

---

Nitpick comments:
In `@Sources/LinguistMac/AppShellModel.swift`:
- Around line 231-244: Replace the actor-based SystemClipboardService with a
main-isolated type by removing `actor` and marking the type `@MainActor` so you
avoid unnecessary actor isolation and still satisfy NSPasteboard's main-thread
requirement; update the declaration of SystemClipboardService (which conforms to
ClipboardServicing) and keep the implementations of readText() and writeText()
but remove the surrounding await MainActor.run { ... } calls so they run on the
main actor naturally and continue to call NSPasteboard.general.string(forType:
.string), clearContents(), and setString(_:forType:) directly.

In `@Sources/LinguistMac/OnboardingView.swift`:
- Around line 78-82: Extract the duplicated settings-opening logic into an
AppShellModel extension method and update both call sites to use it: add an
extension on AppShellModel with a new func openSettingsWindow() that calls
record(.settings), NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil,
from: nil) and NSApp.activate(ignoringOtherApps: true); then remove or replace
the private openSettingsWindow() in OnboardingView and the duplicate code in
MenuBarMenuView to call model.openSettingsWindow() instead so both use the
shared implementation.

In `@Sources/LinguistMac/SettingsView.swift`:
- Around line 50-54: The "Launch at login" Toggle is permanently disabled and
should be hidden or clarified; update the view around Toggle("Launch at login",
isOn: $model.settings.launchAtLoginEnabled) so it is only shown when the feature
flag or milestone is enabled (e.g., wrap it in an if
model.settings.isLaunchAtLoginAvailable) or replace the disabled Toggle with a
non-interactive label that reads "Launch at login (Coming soon)" alongside the
explanatory Text; ensure you remove the .disabled(true) call if using the
"(Coming soon)" label and keep the explanatory caption Text("Launch-at-login
wiring lands with the app preferences milestone.") as-is.

In `@Sources/LinguistMac/TranslationPopupView.swift`:
- Around line 125-146: The user-facing strings in TranslationFailure.displayText
use kind.rawValue and providerID.rawValue; update the .permissionDenied(kind),
.providerUnavailable(providerID) and .missingAPIKey(providerID) branches to use
a human-friendly display name instead (e.g., kind.displayName or a lookup for
PermissionKind and providerID.displayName or a TranslationProviderID-to-name
map) so messages show "Screen Recording" / "DeepL" style labels rather than
technical identifiers; add a displayName computed property or local mapping on
PermissionKind and TranslationProviderID if they don't exist and interpolate
that into the existing cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b748e67a-dc73-4dca-9da4-5a8066e2e1f5

📥 Commits

Reviewing files that changed from the base of the PR and between 25cd6db and 9479162.

📒 Files selected for processing (12)
  • LinguistMac.xcodeproj/project.pbxproj
  • Sources/LinguistMac/AppShellModel.swift
  • Sources/LinguistMac/ContentView.swift
  • Sources/LinguistMac/LinguistMacApp.swift
  • Sources/LinguistMac/MenuBarMenuView.swift
  • Sources/LinguistMac/OnboardingView.swift
  • Sources/LinguistMac/QuickTranslateView.swift
  • Sources/LinguistMac/SettingsView.swift
  • Sources/LinguistMac/TranslationPopupView.swift
  • Sources/LinguistMacCore/AppSettings.swift
  • Sources/LinguistMacCore/AppShellModels.swift
  • Tests/LinguistMacCoreTests/AppShellModelsTests.swift

Comment thread Sources/LinguistMac/MenuBarMenuView.swift
Comment thread Sources/LinguistMac/SettingsView.swift
@Peerapat-J
Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Peerapat-J Peerapat-J merged commit 70a6e0d into dev Jun 5, 2026
6 checks passed
@Peerapat-J Peerapat-J deleted the PJ/m1-ui-shell branch June 5, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: app App target, app identity, lifecycle, and platform integration. area: settings Preferences, onboarding, and user configuration. area: shortcuts Global hotkeys and keyboard-driven workflows. area: translation Translation coordination, language choices, and result state. area: ui SwiftUI/AppKit UI surfaces. enhancement New feature or request priority: p0 Critical path for initial parity. type: feature User-visible product functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant